import { CallStack } from '@/common/CallStack'
import store from '@/apps/panel/store'
import Interpreter from '@/common/InterpreterTree';
import { Macro, MacroRunningStatus, MacroStatus } from '@/types/macro'
import { withPromise } from '@/common/ts_utils'
import { CallStackElement, FrameStatus } from '@/types/call_stack'
import { clone, } from '@/common/ts_utils';
import { and } from '@/common/utils'
import { isExtensionResourceOnlyCommand } from '@/common/command'
import { addLog, editTestCase, setPlayerState } from '@/apps/panel/actions'
import { CallStackEvent } from '@/types/call_stack'
import { interpreter } from './interpreter'
import { player } from './player'
import { monitor } from './monitor'
import Ext from '@/common/web_extension';
import { getCommandResults } from '@/types/macro'
import { getDoneCommandIndices, getErrorCommandIndices, getWarningCommandIndices } from '@/recomputed'
/**
 * 调用栈
 * 用例执行通过调用栈开始
 * 将用例压入栈内开始执行 通过player.play开始执行某个用例
 * 当遇到run命令，将新的用例压入栈内
 * 先执行栈顶的用例，执行完弹出栈，继续执行下一个
 */
export const callStack = new CallStack({
    // 获取单个的测试用例的初始运行状态
    getInitialRunningStatus: () => ({
        nextIndex: 0,
        status: MacroStatus.Running,
        commandResults: [],
        playerState: {} as any,
        interpreterState: {}
    }),
    // 当栈不为空时，调用下一条用例前，暂存当前栈顶的运行状态，以便于恢复执行
    // 备份当前状态
    updateRunningStatusForCaller: (): MacroRunningStatus => {
        // 2、 Restore commandResults
        const playerState = player.getState()
        const reducerState = store.getState()
        const commandResults = getCommandResults({
            ids: playerState.resources.map((command: any) => command.id),
            doneIds: getDoneCommandIndices(reducerState),
            errorIds: getErrorCommandIndices(reducerState),
            warningIds: getWarningCommandIndices(reducerState),
        });
        return {
            playerState, // player的状态
            commandResults, // 命令运行结果
            status: MacroStatus.Running, // 状态
            nextIndex: playerState.nextIndex, // 下一个命令的索引
            interpreterState: interpreter.backupState() // 备份interpreter的状态
        }
    },
    // 用例执行前准备上下文，恢复运行状态
    // 恢复状态
    prepareContext: (resourceAndStatus: CallStackElement<Macro, MacroRunningStatus>, frameStatus: FrameStatus) => {
        // 用例准备阶段，重置player的状态和reducer中的player状态、
        // 重置interpreter的状态     
        const macro = resourceAndStatus.resource as Macro
        const status = resourceAndStatus.runningStatus as MacroRunningStatus

        return withPromise(() => {
            // 1、更新选择的用例
            return store.dispatch(editTestCase(macro.id))
        }).then(() => withPromise(() => ((runningStatus) => {
            // 3、重置player的状态
            // Steps:
            // 1. 还原Player的状态
            // 2. 还原reducer中的player状态
            const { playerState, interpreterState = clone(Interpreter.DefaultState) } = runningStatus

            player.setState(playerState)

            store.dispatch(setPlayerState({
                nextCommandIndex: playerState.nextIndex
            }))
            interpreter.restoreState(interpreterState)
        })(status)))
    },
    // ** This is where the macro starts to run
    // 运行player
    run: (resourceAndStatus: CallStackElement<Macro, MacroRunningStatus>, frameStatus: FrameStatus) => {

        // Steps
        // * Update selected macro
        // * Restore commandResults
        // * Restore player state
        // * Start to play

        // Filter out empty commands
        let opt = resourceAndStatus.runningStatus.playerState;
        const opts = {
            ...opt,
            resources: (opt.resources || []).filter((res: any) => res.cmd && res.cmd.length > 0)
        }
        resourceAndStatus.runningStatus.playerState = opts;
        const macro = resourceAndStatus.resource as Macro
        const status = resourceAndStatus.runningStatus as MacroRunningStatus

        return withPromise(() => {
            const runningStatus = status
            const { isBottom, isResume, frameId } = frameStatus
            // Note: do not use clone here, otherwise will lose `callback` in playerState
            const playerState: any = { ...runningStatus.playerState }
            playerState.noEndEvent = !isBottom

            // Note: frameId in extra will be available in all kinds of player events,
            // frameId is used as id for monitor, so that we can control monitors in player events
            playerState.extra = {
                ...(playerState.extra || {}),
                frameId,
                macroId: macro.id,
                isBottomFrame: isBottom,
                isBackFromCalling: isResume
            }

            Ext.downloads.setUiOptions({ enabled: true })

            if (isResume) {
                // 继续，非第一个栈
                player.setState(playerState)
                // Note: already increase `nextIndex` by one
                player.__setNext(runningStatus.nextIndex)
                return player.play(player.getState())
            } else {

                // 首次调用
                const needDelayAfterLoop = and(
                    ...playerState.resources.map((command: any) => isExtensionResourceOnlyCommand(command.cmd))
                )
                const args = {
                    ...playerState,
                    needDelayAfterLoop
                }

                return player.play(args)
            }

        })
    }
})


// 调用栈监听事件
callStack.on(CallStackEvent.BeforeRun, (macroInfoList) => {
    const lastMacroInfo = macroInfoList[macroInfoList.length - 1]
    const lastName = lastMacroInfo.name
    const prevNames = macroInfoList.slice(0, -1).map((item: any) => `'${item.name}'`)

    if (prevNames.length > 0) {
        store.dispatch(addLog('status', `Running '${lastName}', called by ${prevNames.join(' > ')}`))
    }
})

callStack.on(CallStackEvent.BeforeResume, () => { })

callStack.on(CallStackEvent.AfterReturn, (macroInfoList) => {
    const lastMacroInfo = macroInfoList[macroInfoList.length - 1]
    const lastName = lastMacroInfo.name
    const lastFrameId = lastMacroInfo.frameId
    const prevNames = macroInfoList.slice(0, -1).map((item: any) => `'${item.name}'`)

    monitor.removeTarget(lastFrameId)

    if (prevNames.length > 0) {
        store.dispatch(addLog('status', `Finished running '${lastName}', returning to ${prevNames.join(' > ')}`))
    }
})